[cpp]StringView学习

string_viewcpp17之后提供了一个模板类. 它维护一个对于底层字符数组的只读视图, 可以在多种场景下提高程序的性能.

StringView的优势

传递字符串参数的时候, 在不进行修改操作的时候通常都会使用const string&来接收实参, 其在接收字符串字面值、字符数组和字符串指针的时候还是会存在构造字符串的问题. 即先生成一个匿名string对象, 然后const string&绑定到该匿名对象上去. 当字符串很大时, 会存在严重的性能问题.

substr函数. stringsubstr函数会返回一个string对象. 该操作造成的拷贝会影响程序的性能(在只读要求下).

针对以上问题: string_view能够较好的解决. 因为其是一个只读视图, 用string_view作为参数拷贝的开销是很低的, 以及string_viewsubstr函数返回的还是一个string_view. 避免了重新生成一个新的字符串的大开销操作.

string_viewcpp17以前的一些第三方库中有自己的实现:

  1. leveldbSlice实现.
  2. google基础库AbseilStringView实现.

StringView的实现

字面值

可以使用string_view sv = "just test"sv;字面值来生成string_view.

1
2
3
constexpr string_view operator"" sv(const char* _Str, size_t _Len) noexcept {
return string_view(_Str, _Len);
}

数据成员

string_view的模板类中, 只包含了两个私有数据成员_Mydata_Mysize. _Mydata为指向底层字符数组的指针, 而_Mysize表示string_view的可见长度.

1
2
3
4
5
6
// MSVC-14.30.30705: xstring
// wrapper for any kind of contiguous character buffer
using const_pointer = const _Elem*;
using size_type = size_t;
const_pointer _Mydata;
size_type _Mysize;

构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 默认构造函数
constexpr basic_string_view() noexcept : _Mydata(), _Mysize(0) {}
// 拷贝构造
constexpr basic_string_view(const basic_string_view&) noexcept = default;
// 拷贝赋值
constexpr basic_string_view& operator=(const basic_string_view&) noexcept = default;

// 使用指针和长度进行构造
constexpr basic_string_view(const const_pointer _Cts, const size_type _Count) noexcept // strengthened
: _Mydata(_Cts), _Mysize(_Count) {}

// 只使用指针构造, 不同编译器的实现可能不同.
constexpr basic_string_view(const const_pointer _Ntcts) noexcept //strengthened
: _Mydata(_Ntcts), _Mysize(_Traits::length(_Ntcts)) {}

成员函数

string_view的成员函数几乎和string没有差异. 不过需要注意的是: string_view的成员函数无法修改底层的数据, 比如operator[]返回constexpr引用. 其能修改的只有_Mydata指针的指向以及_Mysize的大小(即string_view的可见范围).

1
constexpr const_reference operator[]( size_type pos ) const;

修改_Mydata指针和_Mysize大小的函数:

1
2
3
4
5
6
7
8
// Moves the start of the view forward by n characters. The behavior is undefined if n > size().
constexpr void remove_prefix( size_type n );

// Moves the end of the view back by n characters. The behavior is undefined if n > size().
constexpr void remove_suffix( size_type n );

// Exchanges the view with that of v.
constexpr void swap( basic_string_view& v ) noexcept;

String和StringView的互相构造

  1. string_view构造string

可以使用string_view直接调用string的构造函数来初始化一个string对象. 并且string不与string_view共享底层数据. 注意只能显式调用string来讲string_view进行转换, 因为其实对底层数据的拷贝.

1
2
3
template< class StringViewLike >
explicit basic_string( const StringViewLike& t,
const Allocator& alloc = Allocator() );

测试代码

1
2
3
4
5
6
7
8
9
10
// 通过字面量创建string_view, 先调用constexpr string_view operator"" sv返回string_view对象, 然后赋值拷贝
string_view sv = "abcdef"sv;
string str = string(sv);

str[0] = '1';
cout << "string = " << str << ", string_view = " << sv << endl;

/*
string = 1bcdef, string_view = abcdef
*/
  1. string构造string_view

string_view可以使用string来构造, 原因是stringstring_view的类型转换函数. 其可以进行隐式的转换, 因为string转换成string_view只是生成了一个只读视图.

1
2
// string -> string_view 的类型转换函数
operator std::basic_string_view<CharT, Traits>() const noexcept;

测试代码

1
2
3
4
5
6
7
string str = "abc";
string_view sv = str;
cout << "string_view = " << sv << endl;

/*
string_view = abc
*/

StringView的使用注意事项

  • 资源所有权的问题:

    It is the programmer’s responsibility to ensure that std::string_view does not outlive the pointed-to character array

    string_view的生命周期和其观察的底层字符串的生命周期是无关的. 因此如果底层字符串先于string_view析构, 那么当再次访问时, 其行为是未定义的.
  • 底层字符串的修改问题:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    string str = "abc";
    string_view sv(str);

    string s = move(str);

    cout << "sv = " << sv << ", str = " << str << ", s = " << s << endl;

    /*
    sv = bc, str = , s = abc
    */

    因此我们需要保证使用string_view观察的底层字符串其必须不能被修改, 否则会造成不可预期的后果.

  • 终结符的问题:

    我们都知道ccpp的字符串是以\0作为终结符的. 而string_view是限定了可见长度, 当我们使用string_view时, 需要注意其不能使用strlen系列的函数进行字符串的操作, 因为字符串终结符的可见性可能被remove_suffix移除掉了.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const string str = "abcdefg";
    string_view sv(str);
    sv.remove_prefix(1);
    sv.remove_suffix(2);

    cout << "sv = " << sv << ", strlen(sv) = " << strlen(sv.data()) << endl;

    /* Error
    sv = bcde, strlen(sv) = 6
    */

总结

string_view解决了只读字符串某些场景下的性能问题. 但其在使用的时候还需要注意其引入的问题, 其不像lock_guardunique_lockshared_ptrweak_ptrunique_ptr等资源管理类一样和被管理资源的生命周期有着较为紧密的联系, 因此在使用的时候需要注意上述的几个问题.

参考

cppreference-string_view
[现代C++]性能控的工具箱之string_view
C++17剖析:string_view的实现,以及性能

作者

Jsss

发布于

2022-04-11

更新于

2022-05-01

许可协议


评论